fix(cpp): TS_2DIFF float/double maxPointNumber once per page (fixes #910) - #901
fix(cpp): TS_2DIFF float/double maxPointNumber once per page (fixes #910)#901kkzi wants to merge 13 commits into
Conversation
|
Thanks for tracking this down. The root-cause analysis is clear, and the new implementation correctly handles Java-compatible prefixes, including overflow prefixes and reads spanning multiple segments. I found one blocking compatibility issue, though: routing FLOAT/DOUBLE batch reads through the scalar decoder regresses legacy raw segments. The scalar prefix detector can misclassify a valid raw header, after which the decoder gets an invalid bit_width_ and spins at end-of-input. I reproduced this for both FLOAT and DOUBLE by encoding 129 sequential raw bit patterns with IntTS2DIFFEncoder / LongTS2DIFFEncoder, then reading them in small batches through the corresponding floating-point decoder. The PR head hangs, while the parent implementation completes successfully. Could we preserve the integer batch path for legacy raw segments, or make the prefix detection unambiguous before switching to the scalar path? It would also be good to add legacy raw batch regression tests for both types. |
ColinLeeo
left a comment
There was a problem hiding this comment.
The overall fix direction looks good, but the legacy raw segment compatibility issue is not fully addressed yet.
The per-block heuristic that distinguished Java-compatible maxPointNumber prefixes from legacy raw delta blocks could misclassify a valid raw header (wi = 0 or bit_width = 0 blocks), which desynced the stream and could spin at end-of-input in batch reads. Decide the page layout once per page instead: parse the whole remaining stream with the Java segment grammar (prefix + overflow bitmaps + block run, validated field ranges and exact exhaustion) and cache the segment prefix offsets. A legacy raw page fails this parse because its first misaligned write_index probe reads >= 0x100. - Legacy raw pages keep the integer SIMD batch decode path with bit-cast semantics (parent-commit behavior). - Java pages consume prefixes only at recorded offsets and take the segment-aware scalar path; this also fixes value semantics across blocks inside one Java segment, which the per-block heuristic could not represent. - Bail out of read_long() when the stream is exhausted with bits still owed, so no residual misconfiguration can loop forever. Also fix ByteStream::check_space(): after set_read_pos() parks the cursor at a page boundary, blindly following read_page_->next_ skipped the boundary page and failed reads with E_OUT_OF_RANGE. Recompute the page from the head instead; page chains are short so the walk is cheap. Add legacy raw batch/scalar/mixed regression tests for FLOAT and DOUBLE (PR apache#901 review).
|
Hi @ColinLeeo, thanks for the thorough review and the reproduction steps — they made this straightforward to chase down. I've pushed Root cause confirmed. Your repro hangs exactly as described: the per-block heuristic ( Fix — unambiguous prefix detection (your option 2). The layout is now decided once per page by Legacy raw batch path preserved (your option 1). Legacy raw pages route through the integer SIMD batch decoder + bit-cast, exactly the parent-commit behavior; Java pages take the segment-aware scalar path. Regression tests. Added for both FLOAT and DOUBLE:
Also hardened One incidental fix this surfaced: Full C++ suite (757 tests) passes, and |
|
Thanks for the review. I have pushed The CI runs for this new commit are currently waiting for approval ( Happy to address any remaining feedback on the legacy-raw compatibility path. |
The pin (a66a679) carries 4 TS_2DIFF float/double fixes not yet merged upstream (PR apache/tsfile#901 open); the branch lives only in the kkzi/tsfile fork. Clones resolving the pin need that fork reachable: git submodule update --init 3rd/tsfile # may fail on the pin git -C 3rd/tsfile remote add fork git@github.com:kkzi/tsfile.git git -C 3rd/tsfile fetch fork a66a6796 git -C 3rd/tsfile checkout a66a6796 Once #901 merges, bump the pin to upstream develop and drop this note.
The pin (a66a679, TS_2DIFF float/double fixes, PR apache/tsfile#901 open) only exists on the fork's fix branch, so the fork is the canonical source until the PR merges. branch = fix/cpp-ts2diff-float-double-batch-prefix. Verified end-to-end: files written by this pin's writer decode correctly through IoTDB 2.0.10's Java tsfile lib (tsfile-2.3.1).
|
One more thought regarding the compatibility design: do we really need to support the historical C++ TS_2DIFF layout?
Then the regression tests can focus on Java ↔ C++ interoperability. |
|
Hi, @kkzi Clarification of the FLOAT/DOUBLE TS_2DIFF Format and the Direction of This FixTL;DR
I reviewed the Java and C++ encoder/decoder implementations and their history again. In the earlier discussion, I mixed together the official Java FLOAT/DOUBLE format, the raw format produced by the early C++ writer, and the per-block prefix format produced by the later C++ writer. I apologize for the confusion. The sections below describe these formats separately and note the remaining boundaries in the current code. 1. How Java Handles FLOAT/DOUBLE TS_2DIFFTS_2DIFF itself encodes integers. Java adds a FLOAT/DOUBLE wrapper around it: Suppose
To distinguish these cases, Java writes one or two page-wide bitmaps when needed. One detail is that Java uses The Java page layout has three forms: The key points are:
2. The Previously Mentioned Raw TS_2DIFF PathBefore #796, the C++ This approach preserves the floating-point bits losslessly. It is not the Java FLOAT/DOUBLE TS_2DIFF format, but it was once the official C++ writer output when FLOAT/DOUBLE + TS_2DIFF was selected explicitly. My earlier regression test used The current writer no longer produces the raw layout, while the reader's handling of it represents compatibility logic for historical C++ files. The raw layout was private to the early C++ implementation and was never supported by the Java reader, so it is not part of the cross-language TsFile format. Detecting the raw and Java layouts from the input bytes retains this historical behavior in the decoder state machine and also introduces format ambiguity. From the perspective of format boundaries and implementation complexity, I prefer focusing the C++ decoder on the canonical Java layout and returning a format error for the earlier raw layout. This simplifies prefix handling and avoids continuing to decode after a misclassification. If the community wants to retain support for early C++ files, that behavior can be discussed separately with a more explicit format identifier. 3. C++ Writer Layout After the Java-Style Wrapper Was Introduced#796 changed C++ FLOAT/DOUBLE TS_2DIFF from raw bit-casting to Java-style scaling, However, the integer encoder triggers a block flush after accumulating 129 values, and As a result, this version of the C++ writer produces a per-block layout. For a page containing three blocks where every block has an overflow value, the layout is: A block without overflow uses: This layout results from the integer block flush and the FLOAT/DOUBLE wrapper flush sharing the same The difference from the Java page-wide layout is the metadata scope. This discussion uses the Java layout as the format baseline: the target C++ encoder/decoder layout has page-wide metadata, while the earlier C++ per-block layout is outside the compatibility scope. The default encoding for FLOAT/DOUBLE is GORILLA. An explicit 4. Format Differences That Remain in the Current PRThis PR uses This part matches Java. One remaining difference concerns metadata scope: The expanded comparison of the three layouts appears in the TL;DR. The current PR removes the repeated The decoder follows the same per-block model. When entering a later block, it clears the bitmap and resets Another related boundary is For canonical Java format compatibility, the current PR has completed the change that writes 5. One Possible Implementation DirectionThe encoder could collect floating-point conversion state across the entire page and generate the flags, bitmaps, and The decoder could parse the FLOAT/DOUBLE metadata once at the beginning of the page and retain the bitmap and current value position throughout the page. When it enters a new integer block, it would continue using the same page-wide bitmap and position. The C++ implementation uses the canonical Java layout as its format baseline. The automatic detection and fallback logic that distinguishes the raw layout, the earlier C++ per-block layout, and the Java layout can be simplified at the same time. The decoder then maintains only the Java format state machine, while other inputs return a format error. The existing batch decoder remains reusable: Per-value bitmap checks and numeric conversion remain, while the main bit unpacking, delta reconstruction, and SIMD batch paths can still be reused. FLOAT/DOUBLE batch reads can also reuse the main flow of the integer batch decoder. 6. Additional Validation for the Current PRThe existing Java/C++ compatibility test can be reused. Relevant locations include:
At present, Java's Two additions can extend this coverage: In other words, the matrices on both sides can include FLOAT/DOUBLE + TS_2DIFF, while the number of written points for every existing compatibility case can be increased to One detail is worth noting: the current compatibility test uses exact-bit comparisons for FLOAT/DOUBLE, while TS_2DIFF applies a fixed-point conversion based on To cover the page-wide bitmap across blocks, the compatibility cases can use A Java fixture with I plan to cover a more complete cross-language compatibility matrix in a separate follow-up PR, including parameterized row counts, data types, encodings, and compression combinations. If increasing the row count reveals compatibility issues in other combinations, those can be tracked in separate issues. |
The per-block heuristic that distinguished Java-compatible maxPointNumber prefixes from legacy raw delta blocks could misclassify a valid raw header (wi = 0 or bit_width = 0 blocks), which desynced the stream and could spin at end-of-input in batch reads. Decide the page layout once per page instead: parse the whole remaining stream with the Java segment grammar (prefix + overflow bitmaps + block run, validated field ranges and exact exhaustion) and cache the segment prefix offsets. A legacy raw page fails this parse because its first misaligned write_index probe reads >= 0x100. - Legacy raw pages keep the integer SIMD batch decode path with bit-cast semantics (parent-commit behavior). - Java pages consume prefixes only at recorded offsets and take the segment-aware scalar path; this also fixes value semantics across blocks inside one Java segment, which the per-block heuristic could not represent. - Bail out of read_long() when the stream is exhausted with bits still owed, so no residual misconfiguration can loop forever. Also fix ByteStream::check_space(): after set_read_pos() parks the cursor at a page boundary, blindly following read_page_->next_ skipped the boundary page and failed reads with E_OUT_OF_RANGE. Recompute the page from the head instead; page chains are short so the walk is cheap. Add legacy raw batch/scalar/mixed regression tests for FLOAT and DOUBLE (PR apache#901 review).
…apache#910) Root cause of apache#910: the C++ FloatTS2DIFFEncoder/DoubleTS2DIFFEncoder wrote the maxPointNumber field (fixed value 2) at every segment boundary, while Java FloatEncoder/DoubleEncoder write it only once at the start of each page. Files written with an empty/short first segment could then be misparsed by Java readers (e.g. TsFileSketchTool crashing on the trailing maxPointNumber). This change aligns the C++ encoder with the Java layout: - Encoder: the maxPointNumber var_uint is now emitted exactly once per page (on reset, before segment 1). Segment boundaries only carry the overflow/underflow FLAG when needed, matching Java's segment grammar. - Decoder: forward-only, prefix-aware parsing that accepts all three page layouts — legacy raw pages (no prefix at all), the new Java format (maxPointNumber only on the first segment), and old C++ per-segment format (backward compatible). The old peek-and-rewind scheme is gone; the segment header of a prefix-free segment is preloaded so decode() never needs to re-read the stream. - Tests: new gtest cases assert the maxPointNumber-once-per-page byte layout for multi-segment pages, scaled-overflow pages (the apache#910 crash scenario), reset() page boundaries, and legacy per-segment backward compatibility. Verified: full C++ test suite passes; Java TsFileSketchTool reads files written by the fixed encoder; tsfile_cli round-trips the data.
…atch) CRT ::open interprets bytes in the active code page; UTF-8 paths with non-ASCII characters fail with E_FILE_OPEN_ERR (28) on machines where the 8.3-shortpath / ACP-transcode workarounds unavailable (8dot3 disabled on the volume, or ACP cannot represent the characters). file_internal::open_utf8 converts UTF-8 -> wide chars -> _wopen, same as the vendored TsFileCpp tree. Applied to ReadFile::open, WriteFile, and RestorableTsFileIOWriter's self-check reader.
…iter windows.h from utf8_file_open.h before decoder_factory.h made INT32/ DATE/DOUBLE ambiguous with using-namespace common in the decoder switch.
get_timeseries_schema built MeasurementSchema with the 2-arg ctor, whose encoding/compression are library defaults (DOUBLE->GORILLA, LZ4) rather than what the file stores. Take both from the first ChunkMeta of the timeseries (chunk metadata is deserialized from the file), falling back to defaults when no chunk metadata is available.
… bytes ChunkMeta entries from the metadata index carry only offsets (C++ deserialization never fills encoding_/compression_type_, unlike Java), so the previous attempt read uninitialized memory. Now: read 256 bytes at the first chunk's offset_of_chunk_header_ and deserialize the ChunkHeader (encoding/compression live there). Adds TsFileIOReader::get_read_file().
…xtures Extend the Java and C++ encoding/compression compatibility matrices with FLOAT + TS_2DIFF and DOUBLE + TS_2DIFF cases and raise every case to 300 rows so pages cross the 129-value TS_2DIFF block boundary (129+129+42), per review feedback on apache#901. The TS_2DIFF value set covers all page layouts the writers can produce: scaled integers, scale-overflow values (reachable at maxPointNumber 2, the C++ writer), and raw IEEE bit patterns (NaN/Infinity, two page-wide bitmaps). Every chosen value restores identically whether the writer used maxPointNumber 0 (Java builder default) or 2 (historical C++ default), so the validating reader never needs to know which writer produced a file; NaN expectations use the canonical Java floatToIntBits pattern. Expected values are computed by applying the writer's tri-state conversion rules, not by reusing the input bits, so non-integer inputs would not round-trip exactly and are excluded. Also add a wire-format contract document derived from the Java FloatEncoder/FloatDecoder/DeltaBinaryEncoder reference implementations (cpp/docs/ts2diff-float-double-wire-format.md), which the upcoming encoder/decoder rework will be validated against. Current state: the six new TS_2DIFF float/double cases fail on the C++ side (write_table returns E_INVALID_ARG and decoded values are misaligned), which is the acceptance baseline the rework must turn green.
Align the C++ FLOAT/DOUBLE TS_2DIFF page layout with the Java canonical format (apache#901 review): - The integer encoder's automatic 129-value block flush now emits a plain integer block into an internal page buffer; overflow flags and buffered blocks survive across the boundary. The page-seal flush emits the page metadata once ([overflow marker][pageValueCount] [page-wide bitmap(s)][maxPointNumber]) followed by all buffered blocks, replacing the per-block wrapper metadata. - Bit width is now the maximum width over the raw deltas rebased by min, mirroring Java calculateBitWidthsForDeltaBlockBuffer, instead of the width of (max - min). When raw deltas wrap the signed type (e.g. adjacent raw IEEE bit patterns), (max - min) wrapped negative and the block was silently written with bit width 0, discarding every delta. - The integer flush now calls the base reset() explicitly so the float wrapper's page-scoped state is not cleared by the virtual dispatch during mid-page block flushes. Verified: Java reads all 30 non-LZMA2 C++ fixtures (including FLOAT/DOUBLE TS_2DIFF at 300 rows across the block boundary with NaN/Infinity raw-bit and scale-overflow pages) bit-exactly; the C++ Java-hex golden tests pass. The 15 remaining generate failures are the LZMA2 compression path failing on this MSVC Debug build regardless of encoding (also reproducible with CHIMP + LZMA2 on develop), tracked separately.
Replace the multi-layout sniffing decoder with a single Java-grammar state machine (apache#901 review): - Page metadata ([overflow marker][pageValueCount][page-wide bitmap(s)] [maxPointNumber], or bare [maxPointNumber]) is parsed exactly once per page and the bitmaps plus page position survive block transitions, so Java multi-block overflow pages decode correctly. - maxPointNumber = 0 (page starting with 0x00, the Java Ts2Diff builder default) is a valid Form 1 page, no longer misdetected as a legacy raw payload. - The raw bit-cast layout and the pre-apache#910 per-segment maxPointNumber layout are rejected as format errors instead of being decoded by heuristic detection; legacy tests now assert fail-fast behavior. - Block headers are validated (write_index in [0,128], bit_width in range) and a truncated header now fails instead of silently reusing stale state, which previously let batch readers spin forever on out-of-format input. FLOAT/DOUBLE batch reads reuse the integer batch decoder (SIMD fast path) and apply the page-wide bitmaps afterwards per page position. Verified all four compatibility directions on the extended matrix (30 non-LZMA2 cases each): C++/Java readers on C++/Java writers, including FLOAT/DOUBLE TS_2DIFF at 300 rows across the block boundary with scaled-overflow and raw-bit pages.
927e43e to
edaf5e8
Compare
|
Hi @ColinLeeo, thanks for the detailed 8/24 clarification — the format baseline and the section-by-section layout analysis made the rework straightforward to scope. The branch is rebased onto develop (the compatibility infrastructure from #905 is now available) and implements the direction you outlined. What changedEncoder (page-wide metadata). The integer encoder's automatic 129-value block flush now emits a plain integer block into an internal page buffer; overflow flags and buffered blocks survive the block boundary. The page-seal flush emits the page metadata once — Bit width. While validating against the Java hex golden tests I found the old width computation Decoder (single grammar, page-wide state). All layout sniffing is removed ( Batch reads. FLOAT/DOUBLE Tests. Both matrices now include VerificationAll four compatibility directions on the extended matrix pass bit-exactly (30 cases each, LZMA2 excluded locally — see below), including FLOAT/DOUBLE TS_2DIFF across the block boundary with page-wide bitmaps. Full C++ suite: 787 tests, 784 pass / 3 skipped (env-gated compat fixtures). Java reads Java's 45 fixtures (LZMA2 included) cleanly. Two things to flag
The commits are structured as: matrix + contract doc, encoder rework, decoder rework. Glad to restructure or address anything else. |
The Java Ts2Diff TSEncodingBuilder hard-codes maxPointNumber = 0 for FLOAT/DOUBLE, so the C++ FloatTS2DIFFEncoder/DoubleTS2DIFFEncoder now default to the same value instead of 2. The wire value is self-describing, but with both writers sharing the default the pages are byte-identical and the C++ writer can no longer produce the scale-overflow form (Form 2), which is unreachable at maxPointNumber 0 - any overflow is a value overflow and takes the raw-bits path. Follow-ups in the same commit: - Java hex goldens regenerated with maxPointNumber 0. - The 0x02-byte-counting assertions are replaced by a structural page walker (metadata once, then a continuous well-formed block stream); byte counting cannot distinguish the 0x00 mpn byte from block-header high bytes. - Ramp data in round-trip tests integerized so expectations hold under the default mpv = 1. - Compatibility-test constants and the wire-format doc updated, with a note that Form 2 pages can only originate from writers configured with mpn > 0. Verified: full C++ suite 784/787 pass; all four compatibility directions green on the 30 non-LZMA2 cases.
|
Follow-up on point 2: Notes from the change:
Re-verified: full C++ suite 784/787 pass, and all four compatibility directions remain green on the 30 non-LZMA2 cases. |
CI clang-format (17.0.6) reorders the includes introduced with the UTF-8 file-open helpers: <fcntl.h> regroups behind the extensionless C++ headers in utf8_file_open.h, and the project includes sort alphabetically in write_file.cc / tsfile_reader.cc. Two long call expressions rewrap at column 80. Note: local clang-format 22 orders the mixed C/C header groups the other way round; this matches the pinned CI version.
Fix C++ TS_2DIFF float/double encoding to match the Java layout, and make the decoder tolerate all three page layouts. Fixes #910.
Summary
FloatEncoder/DoubleEncoder. The Java readers (e.g. TsFileSketchTool) crash on the old layout when a page's first segment is empty/short — that is fix(cpp): Float/DoubleTS2DIFFEncoder writes maxPointNumber per segment, breaking Java FloatDecoder on multi-segment pages #910. The encoder writes maxPN at page start (first encode after reset), so every non-empty page begins with either a FLAG section or the maxPN prefix — this is the invariant the decoder relies on.decode()never rewinds the stream.scan_java_float_double_pagein 1ef5e94). The scan assumed every segment carries a prefix and could not bound segment boundaries on new-format pages (segments 2+ have no prefix and no separator), which made scaled-overflow pages (FLAG + prefix-free continuation) unreadable. The forward-only parser dispatches on the first byte (0x00 / 0x02 / FLAG) with a per-pagepage_first_segment_flag, which handles the new layout unambiguously.check_space()recomputes the read page from the head instead of blindly followingnext_when the cursor is parked at a page boundary (from 1ef5e94). The decoder's probe rewinds (set_read_posfallback branches) rely on this.reset()page boundaries, and legacy per-segment backward compatibility. Legacy raw batch/scalar/mixed regressions from the earlier review are kept.Verification
TsFileSketchToolreads files written by the fixed encoder (previously crashed).tsfile_cliround-trips the data.